---
title: Business rules
summary: Business rules enforced during operation processing.
status: draft
owner: Not determined from source
last_reviewed: 2026-09-22
---

# Business rules

`clsADUserServices` enforces 34 business rules, all of `service` origin — none is inherited from a base class or restated from an embedded or dependency library, because the repository contains no `<ProjectReference>` and no embedded library. The rules span the network-origin authorization gate (BR-01–BR-05), domain and container selection (BR-07–BR-09), the shape of each of the 19 published operations (BR-10–BR-32), and the shared directory-lookup failure behavior (BR-33–BR-34). Statements, triggers, enforcement points and outcomes below are taken verbatim from the analysis catalog.

## Navigation index

| Rule ID | Title | Reached from |
| --- | --- | --- |
| [BR-01](#br-01-mutating-and-password-operations-require-an-approved-caller) | Mutating and password operations require an approved caller | 8 gated operations |
| [BR-02](#br-02-a-local-request-is-always-approved) | A local request is always approved | `CheckAccess` |
| [BR-03](#br-03-an-allow-listed-source-address-is-approved) | An allow-listed source address is approved | `CheckAccess` |
| [BR-04](#br-04-the-caller's-source-address-prefers-the-forwarded-for-header) | The caller's source address prefers the forwarded-for header | `CheckAccess` |
| [BR-05](#br-05-a-denied-request-is-reported-as-a-normal-response) | A denied request is reported as a normal response | 8 gated operations |
| [BR-06](#br-06-read-only-directory-queries-are-not-access-controlled) | Read-only directory queries are not access-controlled | 11 ungated operations |
| [BR-07](#br-07-the-directory-domain-is-selected-by-the-caller) | The directory domain is selected by the caller | every operation except `HelloWorld`/`CheckAccess` |
| [BR-08](#br-08-mutating-operations-always-target-the-external-domain) | Mutating operations always target the external domain | 8 gated operations |
| [BR-09](#br-09-a-new-account-is-created-in-the-cn%3Dusers-container) | A new account is created in the CN=Users container | `AddUser` |
| [BR-10](#br-10-an-empty-password-is-replaced-with-a-hard-coded-default) | An empty password is replaced with a hard-coded default | `AddUser` |
| [BR-11](#br-11-a-new-account-is-provisioned-enabled-with-fixed-identity-fields) | A new account is provisioned enabled with fixed identity fields | `AddUser` |
| [BR-12](#br-12-an-existing-account-is-not-re-created) | An existing account is not re-created | `AddUser` |
| [BR-13](#br-13-an-optional-attribute-is-written-only-when-supplied) | An optional attribute is written only when supplied | `AddUser`, `ChangeUserAccount` |
| [BR-14](#br-14-an-unlock-request-for-an-unknown-user-is-reported-as-such) | An unlock request for an unknown user is reported as such | `UnlockAccount` |
| [BR-15](#br-15-an-account-is-unlocked-only-when-locked-and-the-result-is-re-verified) | An account is unlocked only when locked and the result is re-verified | `UnlockAccount` |
| [BR-16](#br-16-a-group-membership-addition-requires-both-user-and-group-to-exist) | A group-membership addition requires both user and group to exist | `AddUserToGroup` |
| [BR-17](#br-17-adding-an-already-a-member-user-is-a-distinct-outcome) | Adding an already-a-member user is a distinct outcome | `AddUserToGroup` |
| [BR-18](#br-18-deleting-a-non-existent-user-is-a-successful-no-op) | Deleting a non-existent user is a successful no-op | `DeleteUser` |
| [BR-19](#br-19-group-removal-drops-to-the-underlying-directoryentry) | Group removal drops to the underlying DirectoryEntry | `DeleteUserFromGroup` |
| [BR-20](#br-20-a-failed-group-removal-is-reported-as-success) | A failed group removal is reported as success | `DeleteUserFromGroup` |
| [BR-21](#br-21-a-user-exists-when-a-principal-resolves) | A user exists when a principal resolves | `DoesUserExist` |
| [BR-22](#br-22-a-user-has-a-resource-by-exact-transitive-group-name-match) | A user has a resource by exact transitive group-name match | `DoesUserHaveResource` |
| [BR-23](#br-23-an-account-change-updates-only-non-empty-fields) | An account change updates only non-empty fields | `ChangeUserAccount` |
| [BR-24](#br-24-directory-attribute-changes-commit-only-when-modified-is-raised) | Directory attribute changes commit only when modified is raised | `ChangeUserAccount` |
| [BR-25](#br-25-a-password-change-is-applied-via-setpassword-and-committed) | A password change is applied via SetPassword and committed | `ChangeUserPassword`, `ChangeUserPasswordOrGetException` |
| [BR-26](#br-26-two-password-change-variants-differ-only-in-error-strategy) | Two password-change variants differ only in error strategy | `ChangeUserPassword`, `ChangeUserPasswordOrGetException` |
| [BR-27](#br-27-a-directory-attribute-query-returns-the-value-or-userdoesnotexist) | A directory attribute query returns the value or UserDoesNotExist | `GetUserEmail`, `GetUserFAX`, `GetUserPrinter` |
| [BR-28](#br-28-the-group-list-includes-nested-membership) | The group list includes nested membership | `GetUserGroups` |
| [BR-29](#br-29-a-resource-is-a-resource-named-group-filtered-by-caller-supplied-environment) | A resource is a Resource-named group filtered by caller-supplied environment | `GetUserResources` |
| [BR-30](#br-30-last-logon-is-derived-from-the-filetime-attribute) | Last logon is derived from the FILETIME attribute | `GetUserLastLogon` |
| [BR-31](#br-31-the-printer-attribute-is-read-from-the-internal-domain-only) | The printer attribute is read from the internal domain only | `GetUserPrinter` |
| [BR-32](#br-32-group-membership-for-isuseringroup-is-direct-only) | Group membership for IsUserInGroup is direct only | `IsUserInGroup` |
| [BR-33](#br-33-a-group-is-resolved-by-samaccountname%2C-taking-the-first-match) | A group is resolved by SamAccountName, taking the first match | `AddUserToGroup`, `DeleteUserFromGroup`, `DoesUserHaveResource`, `IsUserInGroup` |
| [BR-34](#br-34-a-directory-lookup-failure-is-reported-as-not-found) | A directory lookup failure is reported as not found | every operation except `HelloWorld`/`CheckAccess` |

---

## BR-01 Mutating and password operations require an approved caller

| Field | Value |
| --- | --- |
| Statement | A mutating or password-changing operation may only proceed for an approved caller |
| Trigger | Entry to any of the eight gated operations |
| Enforcement | `If (Not CheckAccess().HasAccess) Then Return AccessDenied()` — `src\ADUserServices\clsADUserServices.asmx.vb` lines 105, 202, 234, 273, 302, 395, 474, 495 |
| Origin | service |
| Reached from | [`AddUser`](contracts/clsaduserservices.md#adduser), [`UnlockAccount`](contracts/clsaduserservices.md#unlockaccount), [`AddUserToGroup`](contracts/clsaduserservices.md#addusertogroup), [`DeleteUser`](contracts/clsaduserservices.md#deleteuser), [`DeleteUserFromGroup`](contracts/clsaduserservices.md#deleteuserfromgroup), [`ChangeUserAccount`](contracts/clsaduserservices.md#changeuseraccount), [`ChangeUserPassword`](contracts/clsaduserservices.md#changeuserpassword), [`ChangeUserPasswordOrGetException`](contracts/clsaduserservices.md#changeuserpasswordorgetexception) |
| Outcome | On denial, an `ADServiceResponse` with `Response = Failure`, `Message = "Access Denied"`, returned as a normal (non-fault) SOAP response |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:105,202,234,273,302,395,474,495` |

---

## BR-02 A local request is always approved

| Field | Value |
| --- | --- |
| Statement | A request originating on the server itself is approved |
| Trigger | `CheckAccess` evaluation |
| Enforcement | `If (HttpContext.Current.Request.IsLocal)` — lines 762–765 |
| Origin | service |
| Reached from | [`CheckAccess`](contracts/clsaduserservices.md#checkaccess) and all eight gated operations |
| Outcome | `HasAccess = True`, `Local = True` in `HasAccessResponse` |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:762-765` |

---

## BR-03 An allow-listed source address is approved

| Field | Value |
| --- | --- |
| Statement | A request whose observed source address exactly matches an entry in the configured allow-list is approved |
| Trigger | `CheckAccess` evaluation |
| Enforcement | `For Each ip In GetAllowedIPs()`, comparing each entry to `GetIncomingIp()`; on a match, sets `response.HasAccess = True` and `response.EnvironmentApi = True` — lines 767–772 |
| Origin | service |
| Reached from | [`CheckAccess`](contracts/clsaduserservices.md#checkaccess) and all eight gated operations |
| Outcome | `HasAccess = True`, `EnvironmentApi = True` |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:767-772` |

---

## BR-04 The caller's source address prefers the forwarded-for header

| Field | Value |
| --- | --- |
| Statement | The caller's source address is taken from `HTTP_X_FORWARDED_FOR`, falling back to `REMOTE_ADDR` only when that header is empty |
| Trigger | Every `CheckAccess` evaluation |
| Enforcement | `GetIncomingIp` — lines 798–806 |
| Origin | service |
| Reached from | [`CheckAccess`](contracts/clsaduserservices.md#checkaccess) and all eight gated operations |
| Outcome | The chosen value is compared in [BR-03](#br-03-an-allow-listed-source-address-is-approved) and returned in `HasAccessResponse.RemoteAddress` |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:798-806` |

---

## BR-05 A denied request is reported as a normal response

| Field | Value |
| --- | --- |
| Statement | A denied request is reported as a normal response carrying a failure code, never as a SOAP fault |
| Trigger | Denial under [BR-01](#br-01-mutating-and-password-operations-require-an-approved-caller) |
| Enforcement | `AccessDenied` — lines 781–789 |
| Origin | service |
| Reached from | the eight gated operations |
| Outcome | `ADServiceResponse{Response=Failure, Message="Access Denied"}` |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:781-789` |

---

## BR-06 Read-only directory queries are not access-controlled

| Field | Value |
| --- | --- |
| Statement | Read-only directory queries are not access-controlled |
| Trigger | Entry to any of the eleven ungated operations |
| Enforcement | Absence of any check in the method bodies |
| Origin | service |
| Reached from | [`HelloWorld`](contracts/clsaduserservices.md#helloworld), [`DoesUserExist`](contracts/clsaduserservices.md#doesuserexist), [`DoesUserHaveResource`](contracts/clsaduserservices.md#doesuserhaveresource), [`GetUserEmail`](contracts/clsaduserservices.md#getuseremail), [`GetUserGroups`](contracts/clsaduserservices.md#getusergroups), [`GetUserResources`](contracts/clsaduserservices.md#getuserresources), [`GetUserFAX`](contracts/clsaduserservices.md#getuserfax), [`GetUserLastLogon`](contracts/clsaduserservices.md#getuserlastlogon), [`GetUserPrinter`](contracts/clsaduserservices.md#getuserprinter), [`IsUserInGroup`](contracts/clsaduserservices.md#isuseringroup), [`CheckAccess`](contracts/clsaduserservices.md#checkaccess) |
| Outcome | The operation executes for any caller that can reach the endpoint |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb` |

---

## BR-07 The directory domain is selected by the caller

| Field | Value |
| --- | --- |
| Statement | The directory domain is selected by the caller's `inExternalDomain` flag: `external.saif.com` when true, `corp.saif.com` when false |
| Trigger | Every directory context creation |
| Enforcement | `GetContext` — lines 823–827, using constants `DOMAIN_EXT`/`DOMAIN_INT` (lines 47–48) |
| Origin | service |
| Reached from | every operation except [`HelloWorld`](contracts/clsaduserservices.md#helloworld) and [`CheckAccess`](contracts/clsaduserservices.md#checkaccess) |
| Outcome | Determines which forest answers the query; invisible in the response |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:823-827` |

---

## BR-08 Mutating operations always target the external domain

| Field | Value |
| --- | --- |
| Statement | Every mutating operation targets the external domain unconditionally; the internal domain is never written |
| Trigger | Entry to any mutating operation |
| Enforcement | Literal `True` passed to `GetContext`/`GetUserPrincipal`/`GetGroupPrincipal`, with the repeated authored comment `'Assume external domain - NetSec will NOT use this` |
| Origin | service |
| Reached from | the eight gated operations |
| Outcome | Internal-domain accounts cannot be created, modified, deleted, unlocked, re-passworded or re-grouped through this service |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb` |

---

## BR-09 A new account is created in the CN=Users container

| Field | Value |
| --- | --- |
| Statement | A new account is created in the `CN=Users` container of the external domain |
| Trigger | `AddUser`, after the existence test |
| Enforcement | `Dim myContainer As String = "CN=Users," & CONTAINER_EXT` (line 109) passed to `GetContext` (line 133) |
| Origin | service |
| Reached from | [`AddUser`](contracts/clsaduserservices.md#adduser) |
| Outcome | The account's distinguished name is fixed to that container; no caller input can change it |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:109,133` |

---

## BR-10 An empty password is replaced with a hard-coded default

| Field | Value |
| --- | --- |
| Statement | When the caller supplies an empty password, a hard-coded default password is used instead |
| Trigger | `AddUser`, before principal population |
| Enforcement | `If inPassword = String.Empty Then inPassword = <literal>` (within lines 118–122) |
| Origin | service |
| Reached from | [`AddUser`](contracts/clsaduserservices.md#adduser) |
| Outcome | The account is created with a known shared credential ([SEC-05](tech-debt.md#critical), CRITICAL). The literal is not reproduced in documentation |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:118-122` |

---

## BR-11 A new account is provisioned enabled with fixed identity fields

| Field | Value |
| --- | --- |
| Statement | A new account is provisioned enabled, with `DisplayName` and `SamAccountName` both set to the supplied user id, and `ScriptPath` set to a single space |
| Trigger | `AddUser`, principal population |
| Enforcement | `With userPrin`, setting `.GivenName`, `.Surname`, `.DisplayName = inUserId`, `.Name`, `.SamAccountName = inUserId`, `.SetPassword(inPassword)`, `.ScriptPath = " "`, `.Description`, and `.Enabled = True` (lines 136–152) |
| Origin | service |
| Reached from | [`AddUser`](contracts/clsaduserservices.md#adduser) |
| Outcome | The created account is immediately usable; `Name` is taken from the separate `inName` parameter |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:136-152` |

---

## BR-12 An existing account is not re-created

| Field | Value |
| --- | --- |
| Statement | An account that already exists is not re-created |
| Trigger | `AddUser`, existence test |
| Enforcement | `userPrin = GetUserPrincipal(inUserId, True)` then `If Not userPrin Is Nothing` (lines 127–131) |
| Origin | service |
| Reached from | [`AddUser`](contracts/clsaduserservices.md#adduser) |
| Outcome | `Response = Failure` with an empty `Message`. The `UserExists` code is assigned and then immediately overwritten, so it never reaches the wire (see [Tech debt](tech-debt.md)) |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:127-131` |

---

## BR-13 An optional attribute is written only when supplied

| Field | Value |
| --- | --- |
| Statement | An optional directory attribute is written only when the corresponding input is non-empty |
| Trigger | `AddUser` and `ChangeUserAccount`, attribute stage |
| Enforcement | `If inX <> String.Empty Then de.Properties("<attribute>").Value = inX`, applied per attribute (`company`, `telephoneNumber`, `mail`, `info`, `title`, `facsimileTelephoneNumber`, `division`) — `AddUser` lines 158–179; `ChangeUserAccount` lines 435–457 |
| Origin | service |
| Reached from | [`AddUser`](contracts/clsaduserservices.md#adduser), [`ChangeUserAccount`](contracts/clsaduserservices.md#changeuseraccount) |
| Outcome | An empty input leaves the existing attribute untouched; there is no way to clear an attribute through this service |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:158-179,435-457` |

---

## BR-14 An unlock request for an unknown user is reported as such

| Field | Value |
| --- | --- |
| Statement | An unlock request for an unknown user is reported as such |
| Trigger | `UnlockAccount` |
| Enforcement | `If (userPrin Is Nothing)` — lines 206–208 |
| Origin | service |
| Reached from | [`UnlockAccount`](contracts/clsaduserservices.md#unlockaccount) |
| Outcome | `Response = UserDoesNotExist`, `Message` naming the user id |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:206-208` |

---

## BR-15 An account is unlocked only when locked and the result is re-verified

| Field | Value |
| --- | --- |
| Statement | An account is unlocked only when it is actually locked out, and the result is re-verified before reporting |
| Trigger | `UnlockAccount` |
| Enforcement | `If (userPrin.IsAccountLockedOut())` calls `userPrin.UnlockAccount()`, then re-checks with `IIf(userPrin.IsAccountLockedOut(), "The account was not unlocked", "The account was unlocked!")` — lines 210–217 |
| Origin | service |
| Reached from | [`UnlockAccount`](contracts/clsaduserservices.md#unlockaccount) |
| Outcome | `Response = Success` in every branch; the distinction ("was unlocked" / "was not unlocked" / "was not locked") is carried only in `Message` free text |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:210-217` |

---

## BR-16 A group-membership addition requires both user and group to exist

| Field | Value |
| --- | --- |
| Statement | A group-membership addition requires both the user and the group to exist |
| Trigger | `AddUserToGroup` |
| Enforcement | Lines 241–250 |
| Origin | service |
| Reached from | [`AddUserToGroup`](contracts/clsaduserservices.md#addusertogroup) |
| Outcome | `Response = UserDoesNotExist` or `GroupDoesNotExist`; no membership change is attempted |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:241-250` |

---

## BR-17 Adding an already-a-member user is a distinct outcome

| Field | Value |
| --- | --- |
| Statement | An attempt to add a user who is already a member is reported as a distinct outcome rather than a failure |
| Trigger | `AddUserToGroup`, exception path |
| Enforcement | `If ex.Message.Contains("already exists")` — lines 256–258 |
| Origin | service |
| Reached from | [`AddUserToGroup`](contracts/clsaduserservices.md#addusertogroup) |
| Outcome | `Response = UserAlreadyInGroup`. Detection depends on an English substring of the directory exception message* |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:256-258` |

---

## BR-18 Deleting a non-existent user is a successful no-op

| Field | Value |
| --- | --- |
| Statement | Deleting a user who does not exist is a successful no-op |
| Trigger | `DeleteUser` |
| Enforcement | `If userPrin IsNot Nothing Then` wraps `userPrin.Delete()` in a `Try`/`Catch`, with no `Else` branch — lines 283–293 |
| Origin | service |
| Reached from | [`DeleteUser`](contracts/clsaduserservices.md#deleteuser) |
| Outcome | `Response = Success`, empty `Message` |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:283-293` |

---

## BR-19 Group removal drops to the underlying DirectoryEntry

| Field | Value |
| --- | --- |
| Statement | Group removal is performed against the underlying `DirectoryEntry` rather than the `GroupPrincipal`, because the managed removal fails on large groups |
| Trigger | `DeleteUserFromGroup`, removal stage |
| Enforcement | `deGroup.Invoke("Remove", New Object() {deUser.Path})` + `deGroup.CommitChanges()` — lines 326–330, with the authored comment at line 325 |
| Origin | service |
| Reached from | [`DeleteUserFromGroup`](contracts/clsaduserservices.md#deleteuserfromgroup) |
| Outcome | The member is removed by distinguished-name path; late-bound invocation (see [Tech debt](tech-debt.md)) |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:325-330` |

---

## BR-20 A failed group removal is reported as success

| Field | Value |
| --- | --- |
| Statement | A failure to remove a user from a group is treated as "the user was probably not in the group" and reported as success |
| Trigger | `DeleteUserFromGroup`, exception path |
| Enforcement | Empty `Catch` with the authored comment `'gobble - user is pribably not in the group` — lines 331–333 |
| Origin | service |
| Reached from | [`DeleteUserFromGroup`](contracts/clsaduserservices.md#deleteuserfromgroup) |
| Outcome | `Response = Success`. A genuine directory failure is indistinguishable from a completed removal (see [Tech debt](tech-debt.md) — TD-20) |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:331-333` |

---

## BR-21 A user exists when a principal resolves

| Field | Value |
| --- | --- |
| Statement | A user "exists" when a principal can be resolved for the supplied id in the selected domain |
| Trigger | `DoesUserExist` |
| Enforcement | Lines 347–352 |
| Origin | service |
| Reached from | [`DoesUserExist`](contracts/clsaduserservices.md#doesuserexist) |
| Outcome | `True` or `False`. A directory failure also yields `False` (see [Tech debt](tech-debt.md) — TD-22) |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:347-352` |

---

## BR-22 A user has a resource by exact transitive group-name match

| Field | Value |
| --- | --- |
| Statement | A user "has" a resource when one of the user's authorization groups has a name exactly equal to the requested resource name |
| Trigger | `DoesUserHaveResource` |
| Enforcement | `For Each grp As GroupPrincipal In userPrin.GetAuthorizationGroups`, checking `If grp.Name = inResourceName` — lines 372–377 |
| Origin | service |
| Reached from | [`DoesUserHaveResource`](contracts/clsaduserservices.md#doesuserhaveresource) |
| Outcome | `Message` = the literal `"True"` or `"False"`; `Response = UserDoesNotExist` when no principal resolves. Matching is on `Name`, not `SamAccountName`, and is exact |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:372-377` |

---

## BR-23 An account change updates only non-empty fields

| Field | Value |
| --- | --- |
| Statement | An account change updates only the fields whose inputs are non-empty, and principal-level fields are saved before directory-level attributes |
| Trigger | `ChangeUserAccount` |
| Enforcement | Lines 408–433 (`GivenName`, `Surname`, `Description`, then `Save()` when `modified`) |
| Origin | service |
| Reached from | [`ChangeUserAccount`](contracts/clsaduserservices.md#changeuseraccount) |
| Outcome | Two separate directory writes per request; a partial failure can leave the two halves inconsistent* |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:408-433` |

---

## BR-24 Directory attribute changes commit only when modified is raised

| Field | Value |
| --- | --- |
| Statement | Directory-level attribute changes are committed only when the `modified` flag was raised |
| Trigger | `ChangeUserAccount`, commit stage |
| Enforcement | `If modified Then de.CommitChanges()` — lines 458–460 |
| Origin | service |
| Reached from | [`ChangeUserAccount`](contracts/clsaduserservices.md#changeuseraccount) |
| Outcome | Changes to `company` and `info` alone never set the flag, so such a request returns `Success` having committed nothing (see [Tech debt](tech-debt.md) — TD-19) |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:458-460` |

---

## BR-25 A password change is applied via SetPassword and committed

| Field | Value |
| --- | --- |
| Statement | A password change is applied by invoking `SetPassword` on the user's underlying `DirectoryEntry` and committing |
| Trigger | `ChangeUserPassword`, `ChangeUserPasswordOrGetException` |
| Enforcement | `ChangePassword` — `de.Invoke("SetPassword", oPassword)` + `de.CommitChanges()`, lines 514–517 |
| Origin | service |
| Reached from | [`ChangeUserPassword`](contracts/clsaduserservices.md#changeuserpassword), [`ChangeUserPasswordOrGetException`](contracts/clsaduserservices.md#changeuserpasswordorgetexception) |
| Outcome | On success `Response = Success`; an unknown user yields `UserDoesNotExist` before any write |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:514-517` |

---

## BR-26 Two password-change variants differ only in error strategy

| Field | Value |
| --- | --- |
| Statement | Two published variants of the password change exist and differ only in error strategy: one absorbs exceptions into a failure code, the other lets them fault |
| Trigger | Caller's choice of operation |
| Enforcement | `ChangeUserPassword` wraps the call in `Try`/`Catch` (lines 478–483) versus `ChangeUserPasswordOrGetException` with no handler (lines 493–501) |
| Origin | service |
| Reached from | [`ChangeUserPassword`](contracts/clsaduserservices.md#changeuserpassword), [`ChangeUserPasswordOrGetException`](contracts/clsaduserservices.md#changeuserpasswordorgetexception) |
| Outcome | `Failure` + exception text, versus a framework SOAP fault carrying the exception |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:478-483,493-501` |

---

## BR-27 A directory attribute query returns the value or UserDoesNotExist

| Field | Value |
| --- | --- |
| Statement | A directory attribute query returns the attribute's value in `Message`, and `UserDoesNotExist` when the user cannot be resolved |
| Trigger | `GetUserEmail`, `GetUserFAX`, `GetUserPrinter` |
| Enforcement | `myStruc.Message = de.Properties.Item("<attr>").Value` — lines 546 (`mail`), 639 (`facsimileTelephoneNumber`), 707 (`extensionAttribute2`) |
| Origin | service |
| Reached from | [`GetUserEmail`](contracts/clsaduserservices.md#getuseremail), [`GetUserFAX`](contracts/clsaduserservices.md#getuserfax), [`GetUserPrinter`](contracts/clsaduserservices.md#getuserprinter) |
| Outcome | The value is returned as a string in `Message` with `Response = Success`; an unset attribute surfaces as `Failure` + a .NET exception message* |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:546,639,707` |

---

## BR-28 The group list includes nested membership

| Field | Value |
| --- | --- |
| Statement | The group list for a user is the names of all of the user's authorization groups, including nested membership |
| Trigger | `GetUserGroups` |
| Enforcement | `For Each grp As GroupPrincipal In userPrin.GetAuthorizationGroups`, calling `userGroups.Add(grp.Name)` — lines 570–574 |
| Origin | service |
| Reached from | [`GetUserGroups`](contracts/clsaduserservices.md#getusergroups) |
| Outcome | An `ArrayList` of group-name strings, untyped on the wire (see [Tech debt](tech-debt.md) — TD-05); empty on any failure |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:570-574` |

---

## BR-29 A resource is a Resource-named group filtered by caller-supplied environment

| Field | Value |
| --- | --- |
| Statement | A "resource" is an authorization group whose name contains `Resource`; groups whose name also contains `TestResource` are returned only for non-production callers, and all other resource groups only for production callers |
| Trigger | `GetUserResources` |
| Enforcement | Lines 596–608 |
| Origin | service |
| Reached from | [`GetUserResources`](contracts/clsaduserservices.md#getuserresources) |
| Outcome | An `ArrayList` of the filtered group names. A production caller receives no `TestResource` group; a non-production caller receives only `TestResource` groups |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:596-608` |

---

## BR-30 Last logon is derived from the FILETIME attribute

| Field | Value |
| --- | --- |
| Statement | Last-logon is derived from the `lastLogonTimestamp` FILETIME and rendered as a date-time string; a value below 1 means the information is unavailable |
| Trigger | `GetUserLastLogon` |
| Enforcement | Lines 664–678 — `HighPart * 4294967296 + lowpart`, `< 1` test, division by `60 * 10000000`, `DateAdd(DateInterval.Minute, lastLogon, #1/1/1601#)` |
| Origin | service |
| Reached from | [`GetUserLastLogon`](contracts/clsaduserservices.md#getuserlastlogon) |
| Outcome | `Message` = the rendered timestamp with `Response = Success`, or `Response = InfoNotAvailable`. Rendering uses the server's culture and time zone* |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:664-678` |

---

## BR-31 The printer attribute is read from the internal domain only

| Field | Value |
| --- | --- |
| Statement | The user's printer is read from `extensionAttribute2` in the internal domain only |
| Trigger | `GetUserPrinter` |
| Enforcement | `GetUserPrincipal(inUserId, False)` (line 700) with the authored comment `'Assume internal domain` |
| Origin | service |
| Reached from | [`GetUserPrinter`](contracts/clsaduserservices.md#getuserprinter) |
| Outcome | External-domain users are always reported as `UserDoesNotExist`; the caller cannot select a domain |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:700` |

---

## BR-32 Group membership for IsUserInGroup is direct only

| Field | Value |
| --- | --- |
| Statement | Group membership for `IsUserInGroup` is direct membership of the resolved group, not transitive authorization-group membership |
| Trigger | `IsUserInGroup` |
| Enforcement | `If groupPrin.Members.Contains(userPrin)` — lines 733–737 |
| Origin | service |
| Reached from | [`IsUserInGroup`](contracts/clsaduserservices.md#isuseringroup) |
| Outcome | `Message` = the literal `"True"`/`"False"`. A user who is a member only through a nested group answers `"False"`, which differs from [BR-22](#br-22-a-user-has-a-resource-by-exact-transitive-group-name-match)/[BR-28](#br-28-the-group-list-includes-nested-membership) on the same underlying question |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:733-737` |

---

## BR-33 A group is resolved by SamAccountName, taking the first match

| Field | Value |
| --- | --- |
| Statement | A group is resolved by matching `SamAccountName` through a `PrincipalSearcher`, taking the first match |
| Trigger | Any group lookup |
| Enforcement | `GetGroupPrincipal` — lines 880–886 |
| Origin | service |
| Reached from | [`AddUserToGroup`](contracts/clsaduserservices.md#addusertogroup), [`DeleteUserFromGroup`](contracts/clsaduserservices.md#deleteuserfromgroup), [`DoesUserHaveResource`](contracts/clsaduserservices.md#doesuserhaveresource), [`IsUserInGroup`](contracts/clsaduserservices.md#isuseringroup) |
| Outcome | An unresolved group yields `GroupDoesNotExist` in the calling operation. `FindOne` makes the result non-deterministic if more than one group matches* |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:880-886` |

---

## BR-34 A directory lookup failure is reported as not found

| Field | Value |
| --- | --- |
| Statement | A directory lookup failure is captured into an internal field and reported to the caller as "not found" |
| Trigger | Any exception inside `GetContext`, `GetUserPrincipal` or `GetGroupPrincipal` |
| Enforcement | `m_LastMessage = ex.Message` + `Return Nothing` — lines 836, 850, 855, 888 |
| Origin | service |
| Reached from | every operation except [`HelloWorld`](contracts/clsaduserservices.md#helloworld) and [`CheckAccess`](contracts/clsaduserservices.md#checkaccess) |
| Outcome | The caller receives `UserDoesNotExist`, `GroupDoesNotExist`, `False`, or an empty list — never the failure detail, which is written to a field no operation reads (see [Tech debt](tech-debt.md) — TD-07, TD-22) |
| Source | `src\ADUserServices\clsADUserServices.asmx.vb:836,850,855,888` |

---

> **Note:** Statements marked with * were inferred from code structure, naming conventions, or
> contextual evidence where direct confirmation was not available in source artifacts. Statements
> marked with ** could not be confirmed; treat as unknown until verified.

## Related documentation
- [Service index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Activation and hosting](activation-hosting.md)
- [Operations](operations.md)
- [Operations and processes](operations-processes.md)
- [Authentication and authorization](authentication-authorization.md)
- [Configuration](configuration.md)
- [Dependencies](dependencies.md)
- [Faults and error pipeline](faults.md)
- [Operational runbook](operational-runbook.md)
- [Tech debt](tech-debt.md)

## Sources
- `src\ADUserServices\clsADUserServices.asmx.vb`
- `.saifdocgen\state\runs\20260922-101326-aduserservices\analysis\aduserservices-analysis.md`
